TypeScriptのコードレビューを依頼された人のための!と?の解説
TypeScriptのコードレビューをしていて、 !
と ?
の意味を改めて確認したら意外とややこしかったのでまとめておきます。
ご注意: 本記事では、nullとundefinedを厳密に区別せず、どちらも含めてnon-null/nullableと表現しています。
Optional Paramater
関数の引数がOptional(省略可能)であることを宣言します。コンパイラはOptional Paramaterが宣言されたとき、その引数の型を T | undefined
と認識します。
勘違いしやすいのが、Nullableを宣言するものではないということです。OptionalとNullableの違いは、Optionalの場合は複数の引数があるときにOptionalの引数の後ろにOptionalでない引数を宣言できないという点です。
function correct(param1: string, param2?: string) { ... } // OK function incorrect(param1?: string, param2: string) { ... } // NG
Non-null assertion operator
TypeScript2.0から導入されました。
Optional Paramaterがnon-nullであるということをコンパイラに明示します。
リリースノートのサンプルコードを引用します。この例で、processEntity関数の引数e?
はOptionalなので、e.name
とするとコンパイラに「eはundefinedの可能性があると」指摘されます。しかし、この例ではvalidateEntity関数でe
がnon-nullであることを保証しているので、そういう場合はe!.name
とnon-nullアサーションを付けることで、コンパイラにe
はnon-nullであると明示することができます。
// Compiled with --strictNullChecks function validateEntity(e?: Entity) { // Throw exception if e is null or invalid entity } function processEntity(e?: Entity) { validateEntity(e); let s = e!.name; // Assert that e is non-null and access name }
ちなみに、以下のようにe
がnon-nullであることをコードで示すことができていれば、non-nullアサーションを付けなくてもコンパイラがnon-nullであることを認識してくれます。
function processEntity(e?: Entity) { if (e) { let s = e.name; } }
Strict Class Initialization
TypeScript2.7から導入されました。
Strict Class Initialization
を有効にすると、クラスのプロパティ変数がconstructor関数で初期化されていない場合にエラーとなります。しかし実際にはconstructor関数以外の場所で初期化されるケースもあります。そんなときにプロパティ変数 !
を付けると、エラーが出ないようになります。つまり、コンパイラに対してこの変数はnon-nullであると宣言しているということです。
リリースノートのサンプルコードを引用します。プロパティ変数foo!: number
はconstructor関数では初期化されていませんが、initialize()
で初期化されていることがわかっているのでfoo
はnon-nullであると宣言します。
class C { foo!: number; // ^ // Notice this '!' modifier. // This is the "definite assignment assertion" constructor() { this.initialize(); } initialize() { this.foo = 0; } }
プロパティ変数をfoo?: number
やfoo: number | undefined
とすることもできますが、その場合は参照時にnon-nullアサーションを付けてfoo!
としないとnullableであると判断されるので、宣言時にnon-nullを宣言できたほうが良いでしょう。
(´-`).。oO( ところでClassのプロパティ変数に?
を付けられるというのが公式ドキュメントを探しても見つからない。Interfaceには書いてあるのだけど。
Definite Assignment Assertions
Definite Assignment Assertions
同じく、TypeScript2.7から導入されました。
Strict Class Initializationと同じように、関数内でletで変数が宣言されたとき、それが関数のスコープ内で代入されずに参照されようとしたときコンパイラはエラーとみなします。このとき、letの変数にDefinite Assignment Assertionsの!
を付けることで、コンパイラにnon-nullであることを明示します。
リリースノートのサンプルコードを引用します。let x!: number
の!
がDefinite Assignment Assertionsです。これがついていない場合は、コンパイラは「x
はまだ代入されていない」としてエラーとします。
// Notice the '!' let x!: number; initialize(); // No error! console.log(x + x); function initialize() { x = 10; }
なお、Definite Assignment Assertionsを付ける代わりに、参照時にNon-null assertion operatorを付けることでも回避は可能です。
let x: number; initialize(); // No error! console.log(x! + x!); function initialize() { x = 10; }
まとめ
基本的には?
で宣言された場合はnullやundefinedの可能性がある、!
がついている時はnullやundefinedの可能性はないと認識しておけば良さそうです。
個人的に紛らわしいと思うのが、?
で宣言された変数がifなどでnullやundefinedでないと文脈で判断できるときに、!
(Non-null assertion operator)を付けなくてもよいという点です。キャストされてないのに文脈で型が変わるというのが私には気持ち悪いと思いました。このへんはチームでポリシーを決めておくと良いかと思います。